Spring Web MVC 와 Spring WebFlux의 근본적인 비교
프로젝트를 진행하면서 스프링 게이트웨이를 만들었는데, 이 때 참고한 사례에서 webflux로 구현한 몇몇 사례들을 봐서 간단하게 찾아본 차이를 알아보겠습니다. 큰 틀에서 봤을 때, Spring MVC와 WebFlux는 Spring 생태계 내에서 웹 애플리케이션을 구축할 수 있는 프레임워크들입니다. 하지만 이들은 실행 방식과 설계 철학에서 근본적으로 다릅니다. 핵심 차이는 바로 **블로킹(Blocking) 대 논블로킹(Non-blocking)**에 있습니다.
핵심 개념: 커피숍 비유
이 차이를 이해하기 위해 다음과 같은 커피숍 시나리오를 생각해 봅시다.
시나리오 A: 블로킹 바리스타(Spring Web MVC)
-
고객이 카운터에 옵니다.
-
바리스타가 주문을 받고, 커피를 직접 만들어 고객에게 줍니다.
-
중요한 것은, 이 바리스타가 커피가 완성될 때까지 아무 것도 하지 않습니다. 즉, "블로킹" 되어 있습니다.
-
첫 고객이 완전히 커피를 받을 때까지 다음 고객의 주문은 대기해야 합니다.
-
더 많은 고객을 처리하려면, 더 많은 바리스타를 고용해야 합니다. 각 바리스타는 한 고객을 전담합니다.
이것이 바로 명령형(Imperative), 요청당 스레드 모델입니다. 간단하고 직관적이지만, 많은 바리스타(스레드)가 대기하는 동안 자원이 낭비될 수 있습니다.
시나리오 B: 논블로킹 바리스타(Spring WebFlux)
-
고객이 카운터에 옵니다.
-
바리스타가 주문을 받고 자동 커피 기계에 맡깁니다.
-
중요하게도, 바리스타는 커피가 완성될 때까지 기다리지 않고 바로 다음 주문을 받으러 갑니다. 즉, "논블로킹" 되어 있습니다.
-
커피가 완성되면, 기계가 알림(콜백)을 보내고, 바리스타가 완성된 커피를 고객에게 전달합니다.
-
한 명의 바리스타가 매우 효율적으로 동시에 여러 주문을 처리할 수 있습니다.
이것이 바로 리액티브(Reactive), 이벤트 루프(Event-Loop) 모델입니다. 조율이 복잡하지만 자원 활용이 매우 효율적이며, 한 사람이 여러 일을 동시에 처리할 수 있습니다.
Spring Web MVC (블로킹 바리스타)
전통적인 Spring 웹 프레임워크로, 10년 이상 널리 사용되었습니다.
-
프로그래밍 모델: 명령형(Imperative)
순차적으로 명령을 작성하는 코드 스타일입니다.
예:@GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { // 1. 데이터베이스 호출(스레드가 여기서 블로킹됨) User user = userRepository.findById(id); // 2. 결과 반환 return user; } -
스레드 모델: 요청당 스레드 할당(Thread-per-Request)
- Tomcat 같은 서블릿 컨테이너 위에서 동작합니다.
- 보통 200개 정도의 워커 스레드 풀을 유지합니다.
- 요청이 들어오면 스레드가 할당되어 끝날 때까지 해당 요청을 처리합니다.
- 네트워크 호출이나 DB 쿼리 같은 I/O 작업을 할 때, 스레드는 해당 작업이 끝날 때까지 '대기(블로킹)'합니다.
- 200개의 동시 요청이 있다면 200개의 스레드가 필요하며, 201번째 요청은 대기합니다.
-
적용 시기:
- CRUD API, 서버 사이드 렌더링 페이지에 적합
- CPU 집약적 작업 위주인 경우
- 개발팀이 전통적 명령형 프로그래밍에 익숙할 때
- JPA/Hibernate와 같은 블로킹 라이브러리를 사용할 때
Spring WebFlux (논블로킹 바리스타)
Spring 5에서 도입된 최신 리액티브 웹 프레임워크입니다.
-
프로그래밍 모델: 리액티브(Reactive)
스트림 형태의 데이터를 대상으로 선언적 파이프라인을 작성합니다.
Mono(0 또는 1개 아이템) 및Flux(0~N개 아이템)라는 리액티브 타입을 사용합니다.@GetMapping("/user/{id}") public Mono<User> getUser(@PathVariable Long id) { // 1. 나중에 사용자 조회가 완료될 것을 알리는 “퍼블리셔” 선언 return userRepository.findById(id); // DB 호출이 비동기적으로 처리됩니다. } -
스레드 모델: 이벤트 루프(Event Loop)
- 보통 Netty 기반의 논블로킹 서버 위에서 실행됩니다.
- CPU 코어당 하나씩 적은 수의 이벤트 루프 스레드를 사용합니다.
- 요청이 들어오면 이벤트 루프 스레드가 즉시 핸들링을 시작합니다.
- I/O 작업(예: 비동기 DB 호출)이 필요하면, 이를 워커 스레드 풀에 맡기고 콜백을 설정합니다. 그 사이 이벤트 루프 스레드는 다른 요청을 즉시 처리합니다.
- I/O 작업 완료 시, 콜백을 통해 이벤트 루프 스레드가 작업을 이어서 처리합니다.
- 따라서 적은 수의 스레드로 수많은 동시 요청을 처리할 수 있습니다.
-
적용 시기:
- 대량 동시 접속 및 고처리량 API에 최적
- 스트리밍 API, 실시간 전송(예: 영상, 증권 시세)
- 최대 확장성과 자원 효율이 필요할 때
요약 표
| 특징 | Spring Web MVC (블로킹) | Spring WebFlux (논블로킹) |
|---|---|---|
| 프로그래밍 패러다임 | 명령형 | 리액티브 |
| 스레드 모델 | 요청당 전용 스레드 | 이벤트 루프 |
| 동시성 처리 | 스레드 풀 사이즈에 제한 | 매우 높은 동시성 지원 |
| 대표 서버 | Tomcat (서블릿 API 기반) | Netty (리액티브 스트림 기반) |
| 주요 의존성 | spring-boot-starter-web |
spring-boot-starter-webflux |
| 코드 스타일 | 직접 객체 반환 (User) |
Mono<User>, Flux<User> 반환 |
| 적합 대상 | 전통적 CRUD, 블로킹 라이브러리 사용 | API 게이트웨이, 스트리밍, 고동시성 API |
프로젝트에서 API 게이트웨이는 수많은 연결을 효율적으로 처리해야 하므로 WebFlux를 사용하고, 사용자 서비스는 비즈니스 로직과 DB 작업 위주이기에 Spring MVC를 사용합니다.